cmake_minimum_required(VERSION 3.12)
project(libfive)

# Properly distinguish between Apple and upstream Clang
cmake_policy(SET CMP0025 NEW)
include(CheckCXXCompilerFlag)

option(BUILD_STUDIO_APP "Build Studio application" ON)
option(BUILD_GUILE_BINDINGS "Build Guile bindings" ON)
option(BUILD_PYTHON_BINDINGS "Build Python bindings" ON)

option(LIBFIVE_PACKED_OPCODES
    "Tightly pack opcodes (breaks compatibility with older saved f-reps)"
    OFF)

if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE RELEASE)
endif()

if (LIBFIVE_PACKED_OPCODES)
    message("Using packed opcodes, which breaks compatibility with saved f-reps!")
    add_definitions(-DLIBFIVE_PACKED_OPCODES)
endif()

# Build libfive with ccache if the package is present
set(LIBFIVE_CCACHE_BUILD OFF CACHE BOOL "Set to ON for a ccache enabled build")
if(LIBFIVE_CCACHE_BUILD)
  find_program(CCACHE_PROGRAM ccache)
  if(CCACHE_PROGRAM)
      set(LIBFIVE_CCACHE_MAXSIZE "" CACHE STRING "Size of ccache")
      set(LIBFIVE_CCACHE_DIR "" CACHE STRING "Directory to keep ccached data")
      set(LIBFIVE_CCACHE_PARAMS "CCACHE_CPP2=yes CCACHE_HASHDIR=yes"
          CACHE STRING "Parameters to pass through to ccache")

      set(CCACHE_PROGRAM "${LIBFIVE_CCACHE_PARAMS} ${CCACHE_PROGRAM}")
      if (LIBFIVE_CCACHE_MAXSIZE)
        set(CCACHE_PROGRAM "CCACHE_MAXSIZE=${LIBFIVE_CCACHE_MAXSIZE} ${CCACHE_PROGRAM}")
      endif()
      if (LIBFIVE_CCACHE_DIR)
        set(CCACHE_PROGRAM "CCACHE_DIR=${LIBFIVE_CCACHE_DIR} ${CCACHE_PROGRAM}")
      endif()
      set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE_PROGRAM})
  else()
    message(FATAL_ERROR "Unable to find the program ccache. Set LIBFIVE_CCACHE_BUILD to OFF")
  endif()
endif()

################################################################################

if(NOT MSVC)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -g -fPIC -pedantic -Werror=switch")

    # Sometimes this flag is not supported (e.g. on Apple Silicon)
    check_cxx_compiler_flag("-march=native" MARCH_SUPPORTED)
    if(MARCH_SUPPORTED)
        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native")
    endif()

    set(CMAKE_CXX_FLAGS_DEBUG "-O0")
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3 -DEIGEN_NO_DEBUG")
else()
    set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
    set(CMAKE_CXX_FLAGS "/EHsc /WX /D_USE_MATH_DEFINES /D_SCL_SECURE_NO_WARNINGS")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4267 /wd4244 /wd4305")
    set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /MD/ /DEIGEN_NO_DEBUG /arch:AVX2")
    set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /MDd")
endif()

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

if(APPLE)
    set(CMAKE_MACOSX_RPATH ON)
endif()

# Work around an issue with Boost::Interval on OpenBSD and MinGW on Windows
if ("${CMAKE_SYSTEM_NAME}" STREQUAL "OpenBSD" OR MINGW)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D__USE_ISOC99")
endif()

################################################################################
# Find all packages here at the top level so we can print debugging info
if(NOT MSVC)
    # These packages have different naming conventions in MSVC vs Linux
    find_package(Boost REQUIRED)
    find_package(PkgConfig REQUIRED)

    pkg_check_modules(GUILE guile-2.2>=2.2.1)
    if (NOT(GUILE_FOUND))
        pkg_check_modules(GUILE guile-3.0>=3.0.0)
    endif()

    pkg_check_modules(EIGEN REQUIRED eigen3>=3.2.92)
else()
    find_package(eigen3 REQUIRED)
    find_package(boost REQUIRED 1.65.0)
endif()

# These packages have consistent names across OS's
find_package(PNG REQUIRED)
find_package(Python3 COMPONENTS Interpreter Development)

# Both Windows and Linux will attempt to build the Studio UI
if (BUILD_STUDIO_APP)
    find_package(Qt5Core 5.12)
    if (Qt5Core_FOUND)
        set(QtCore_FOUND 1)
    else()
        find_package(Qt6Core 6.0)
        if (Qt6Core_FOUND)
            set(QtCore_FOUND 1)
        endif()
    endif()
endif()

if (UNIX AND NOT(APPLE))
    find_package(Threads REQUIRED)
    set(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} -latomic")
endif(UNIX AND NOT(APPLE))

################################################################################
# Inform the user which subsystems will be built and any missing deps
message("Checking dependencies:")
message("  libfive:		✓")

if (BUILD_GUILE_BINDINGS)
  if (GUILE_FOUND)
    message("  Guile bindings:	✓")
  else ()
    message("  Guile bindings:	✘   (needs Guile 2.2 or later)")
  endif()
else()
    message("  Guile bindings:	✘   (skipping)")
endif()

if (BUILD_PYTHON_BINDINGS)
  if (Python3_FOUND)
    message("  Python bindings:	✓")
  else()
    message("  Python bindings:	✘   (Python 3 not found)")
  endif()
else()
  message("  Python bindings:	✘   (skipping)")
endif()

if (BUILD_STUDIO_APP)
  if (GUILE_FOUND AND BUILD_GUILE_BINDINGS)
    set(STUDIO_WITH_GUILE ON)
  else()
    set(STUDIO_WITH_GUILE OFF)
  endif()

  if (Python3_FOUND AND BUILD_PYTHON_BINDINGS)
    set(STUDIO_WITH_PYTHON ON)
  else()
    set(STUDIO_WITH_PYTHON OFF)
  endif()

  if (QtCore_FOUND AND STUDIO_WITH_GUILE AND STUDIO_WITH_PYTHON)
    message("  Studio:		✓   (Python + Guile)")
  elseif (QtCore_FOUND AND STUDIO_WITH_GUILE)
    message("  Studio:		✓   (Guile only)")
  elseif (QtCore_FOUND AND STUDIO_WITH_PYTHON)
    message("  Studio:		✓   (Python only)")
  else ()
    if (QtCore_FOUND)
      message("  Studio:		✘   (needs Guile or Python)")
    elseif (STUDIO_WITH_GUILE OR STUDIO_WITH_PYTHON)
      message("  Studio:		✘   (needs Qt 5.12 or later)")
    else()
      message("  Studio:		✘   (needs Qt 5.12 or later, and Guile or Python")
    endif()
  endif()
else()
    message("  Studio:		✘   (skipping)")
endif()

################################################################################

# Set a flag to detect the case where users run CMake in the wrong directory
set(LIBFIVE_BUILD_FROM_ROOT true)

# Always build the kernel and test suite
add_subdirectory(libfive)

if(BUILD_STUDIO_APP AND (STUDIO_WITH_GUILE OR STUDIO_WITH_PYTHON) AND QtCore_FOUND)
    add_subdirectory(studio)
endif()
